package multidraw.model;

import java.awt.Color;
import java.io.FileInputStream;
import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Properties;
import java.util.Vector;

import javax.swing.JOptionPane;

import multidraw.model.commands.AddShapeCommand;
import multidraw.model.commands.Command;
import multidraw.model.server.CanvasModel;
import multidraw.model.server.CoreData;
import multidraw.model.server.Role;
import multidraw.tools.SelectorTool;
import multidraw.tools.Tool;


/**
 * This class serves as the local cache of the remote model. It helps 
 * accelerating the communication between the central server and the clients.
 * Reduce network traffic.
 * @composed 1 updateContent() * ModelObserver
 * @has - - - CanvasModel
 */
public class DrawingCanvasModel implements CentralCanvasObserver, Canvas {
	protected CanvasModel canvasModel;
	List<Shape> data = new ArrayList<Shape>();
	List<ModelObserver> observers = new ArrayList<ModelObserver>();
	String clientID;
	long revisionID;
	String sessionID;
	Role role;
	Hashtable<String, Role> userRoles;
	
	private Tool currentTool;
	private Color penColor = Color.black;
	
	//Shape selectedObject = null;
	int selectedObjectIndex = -1;
	
	public DrawingCanvasModel(CanvasModel model) {
		this.canvasModel = model;
		
	}
	
	/**
	 * Constructor for offline mode.
	 */
	public DrawingCanvasModel() {
		//this.canvasModel = model;		
	}
	
	public Shape getSelectedObject() {
		if (selectedObjectIndex >= 0 && selectedObjectIndex < data.size())
			return data.get(selectedObjectIndex);
		else return null;
	}
	
	public int getSelectedObjectIndex() {
		return selectedObjectIndex;
	}

	public void attachObserver(ModelObserver o) {
		observers.add(o);
	}
	
	public void notifyObservers() {
		for (ModelObserver o: observers) {
			o.updateContent();
		}
	}
	
	public List<Shape> getData() {
		return data;
	}
	
	public void addShape(Shape s) {
		AddShapeCommand command = new AddShapeCommand(s, revisionID + 1);
		addCommand(command);
	}
	
	public void addCommand(Command command) {
		if (canvasModel == null) {
			//offline mode
			this.applyCommandToCoreData(command);
			notifyObservers();
		} else {
			// online mode
			// add the sessionID before sending to the server
			command.setSessionID(this.sessionID);
			
			try {
				//canvasModel.addShape(s);
				canvasModel.applyCommand(command);
			} catch (Exception e) {
				System.err.println(e);
			}
		}
	}
	
	public void selectObject(int X, int Y) {
		for (int i = data.size() - 1; i >= 0; i--) {
			Shape s = data.get(i);
			
			if (s.isInsideSelectionalArea(X, Y)) {
				//selectedObject = s;
				selectedObjectIndex = i;
				//JOptionPane.showMessageDialog(null, "Selected");
				notifyObservers();
				return;
			}
		}
		
		selectedObjectIndex = -1; //no object selected
		//JOptionPane.showMessageDialog(null, "None Selected");
		notifyObservers();
	}
	
	public void setCurrentTool(Tool t) {
		currentTool = t;
		if (!(currentTool instanceof SelectorTool)) {
			selectedObjectIndex = -1; //deselect all objects.
		}
		notifyObservers();
	}
	
	public Tool getCurrentTool() {
		return currentTool;
	}
	
	public void clearCanvas() {
		data.clear();
		//fixed the null pointer exception in getSelectedObject.
		selectedObjectIndex = -1;
		//update all View
		notifyObservers();
	}
	//public void fillShape
	public Color getPenColor() {
		return penColor;
	}
	
	public void setPenColor(Color c) {
		System.out.println("Canvas color change ...");
		System.out.println("color = " + c);
		penColor = c;
		notifyObservers();
	}
	
	public void deleteSelectedObject() {
		if (selectedObjectIndex >= 0) {
			data.remove(selectedObjectIndex);
			selectedObjectIndex = -1;
		}
		notifyObservers();
	}
	
	/**
	 * Switch filling option of currently selected object
	 */
	public void switchSelectedObjectFilling(){
		if (getSelectedObject() != null) {
		    getSelectedObject().switchFilling();
		    notifyObservers();
		}
	}
	
	/**
	 * The central server uses this method to call back the client to
	 * update new changes.
	 */
	public void updateModelContent() {
		//refresh the data from server
		try {
			//data = new ArrayList<Shape>(canvasModel.getData());
			List<Command> updates = canvasModel.getUpdates(clientID);
			if (updates != null) {
				for (Command c: updates) {
					this.applyCommandToCoreData(c);
				}
			}
		} catch (RemoteException e) {
			System.err.println(e);
		}
		notifyObservers();
		//TODO: Be very careful here about the situation when this user trying
		//to do sth with the shape but some one else update the model.
		//Then this method invoked and may break the current local canvas.
		//The good news is that there is always only one guy can modify the canvas.
		//So this situation be not appear. But we have to investigate it.
	}
	
	public void applyAddShape(Shape s) {
		data.add(s);
		System.out.println("Add a new shape of the ID: " + s.getShapeID());
	}
	
	public void setName(String n) {
		clientID = n;
	}
	
	public String getName() {
		return clientID;
	}
	
	public void applyCommandToCoreData(Command c) {
		revisionID++;
		c.setCanvas(this);
		c.execute();
	}
	
	public long getNextID() {
		long id = 1;
		
		for (Shape s: data) {
			if (id < s.getShapeID()) id = s.getShapeID();
		}
		
		return id + 1;
	}
	
	public void applyAddChar(long shapeID, char ch) {
		for (Shape s: data) {
			if (s.getShapeID() == shapeID) {
				TextBox t = (TextBox) s;
				t.addChar(ch);
				return;
			}
		}
	}
	
	public List<Shape> getShapes() {
		return data;
	}
	
	public void setSelectedObjectIndex(int index) {
		selectedObjectIndex = index;
	}
	
	public void setClientID(String n) {
		clientID = (n != null && !n.equals("")) ? n : clientID;
	}

	public void applyAddUser(String name) {
		// TODO Auto-generated method stub
		
	}
	
	public void login() throws IOException {
		canvasModel = connectToServer();
		
		if (canvasModel == null) {
			throw new IOException("Cannot connect to the server.");
		}
		
		SecureRandom rand = new SecureRandom();
		try {
			UnicastRemoteObject.exportObject(this, rand.nextInt(10000) + 10000);
			int loginResult = canvasModel.attachObserver(this);
			if (loginResult != 0)
				throw new IOException(
						"Cannot login with this username. Some one picked it.");
			//CoreData coreData = canvasModel.getCoreData();
			//this.data = coreData.shapes;
			//this.revisionID = coreData.revisionID;
		} catch (Exception e) {
			System.err.println("Client exception: " + e.toString());
			e.printStackTrace();
		}
	}
	
	protected CanvasModel connectToServer() {
		//Connect to the server
		CanvasModel stub = null;
		
		try {
			// Read properties file.
			Properties properties = new Properties();
			try {
			    properties.load(new FileInputStream("multidraw.conf"));
			} catch (IOException e) {
			}
			
			String serverip = properties.getProperty("server");
			if (serverip == null) serverip = "localhost";
			
			System.out.println("connecting to the server : " + serverip);
			Registry registry = LocateRegistry.getRegistry(serverip);
			stub = (CanvasModel) registry.lookup("CanvasModel");
			
		} catch (Exception e) {
			System.err.println(e);
		}
		
		return stub;
	}
	
	//create a session and linked this local cache to that session.
	public void createSession() throws IOException {
		if (canvasModel == null) throw
			new IOException("Please connect to the server.");
		
		try {
			sessionID = canvasModel.createSession(clientID);
		} catch (RemoteException e) {
			throw new IOException("Remote exception: " + e.getMessage());
		}
		
		if (sessionID == null) throw new IOException("Cannot create the session");
		
		role = Role.OWNER;
		
		CoreData coreData = canvasModel.getCoreData(sessionID);
		this.data = coreData.shapes;
		this.revisionID = coreData.revisionID;
		
		notifyObservers();
	}
	
	public void joinSession(String ownerID) throws IOException {
		if (canvasModel == null) throw
			new IOException("Please connect to the server.");
		
		try {
			sessionID = canvasModel.joinSession(clientID, ownerID);
		} catch (RemoteException e) {
			throw new IOException("Remote exception: " + e.getMessage());
		}
	
		if (sessionID == null) throw new IOException("Cannot join the session");
		
		role = Role.COLLABORATOR;
		
		CoreData coreData = canvasModel.getCoreData(sessionID);
		this.data = coreData.shapes;
		this.revisionID = coreData.revisionID;		
		
		notifyObservers();
	}
	
	public Vector<String> getClientList() {
		Vector<String> users = new Vector<String>();
		
		if (sessionID == null) {
			return null;
		}
		
		if (userRoles == null) {
			try {
				userRoles = canvasModel.getUserRoles(sessionID);
			}
			catch (Exception e) {
				e.printStackTrace();
			}
		}
		
		for (String n : userRoles.keySet()) {
			users.add(n + " - " + userRoles.get(n));
		}
		
		return users;
	}
}
